<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Todo.HTML - Simple, Fast and Fancy Task Management - v0.1.20080724</title>
    <!--
        Website: http://code.google.com/p/todohtml
        Author : Ning Wang (Hexadecimus@gmail.com)
        Licence: WTHIL (What The Hell Is Licence)
        Release: 0.1.0, 07/24/2007
    //-->
    <style type="text/css">
      div#actions tr.even { background-color:#DDDDDD }
      div#actions tr.odd  { background-color:#EEEEEE }

      /* action operations */
      span.op {
        text-decoration:underline;
        font-size:small;
        color:blue;
      }
      span.op:hover {
        cursor:pointer;
      }

      span.view_button {
        text-decoration:underline;
        color:blue;
        cursor:pointer;
      }

      div#actions th { 
        background-color: #AAAAAA;
        color:white;
        font-weight:bold;
      }

      div#actions *.priority {
        cursor:pointer;
        font-weight:bold;
      }
      div#actions span.urgent {
        color:red;
      }
      div#actions span.description.completed {
        text-decoration:line-through;
      }
      div#actions span.completed {
        color:gray;
      }

      /* highlight tags */
      div#actions span.tagname  {
        font-weight:bold;
        color:green;
        font-size:small;
        text-decoration:underline;
        cursor:pointer;
      }
      div#actions span.tagvalue  {
        font-style:italic;
        color:green;
        font-size:small;
        text-decoration:underline;
      }
      /* un-highlight tags when action is checked */
      div#actions td.completed span.tagname  {
        font-weight:bold;
        color:gray;
        font-size:small;
        text-decoration:underline;
      }
      div#actions td.completed span.tagvalue  {
        font-style:italic;
        color:gray;
        font-size:small;
        text-decoration:underline;
      }
    </style>
    <script>
    <!--
    // Load file using HTTP request. Copied from:
    //     http://www.jibbering.com/2002/4/httprequest.html
    function createHttpRequest() {
      var xmlhttp=false;
      /*@cc_on @*/
      /*@if (@_jscript_version >= 5)
      // JScript gives us Conditional compilation, we can cope with old IE versions.
      // and security blocked creation of the objects.
      try {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
      } catch (e) {
        try {
          xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        } catch (E) {
          xmlhttp = false;
        }
      }
      @end @*/
      if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
        try {
          xmlhttp = new XMLHttpRequest();
          // alert("Succeeded: XMLHttpRequest");
        } catch (e) {
          alert("Failed: XMLHttpRequest");
          xmlhttp=false;
        }
      }
      if (!xmlhttp && window.createRequest) {
        try {
          xmlhttp = window.createRequest();
          // alert("Succeeded: window.createRequest");
        } catch (e) {
          xmlhttp=false;
          alert("Failed: window.createRequest");
        }
      }
      return xmlhttp;
    }

    function sendHttpRequest(filename, callback) {
      var xmlhttp = createHttpRequest();
      if (xmlhttp) {
        xmlhttp.open("GET", filename, true);
        xmlhttp.onreadystatechange=function() {
          if (xmlhttp.readyState==4) {
            callback(xmlhttp.responseText);
          }
        }
        xmlhttp.send(null)
      } else {
        alert("Cannot create HTTP request.");
      }
    }

    function parseDataFileContent(content) {
      clearActions();
      lines = content.split("\n");
      for (i in lines) {
        if (/^\s*#/.test(lines[i])) {
          // Skip comments
        } else if (/^\s*$/.test(lines[i])) {
          // Skip empty lines
        } else {
          var action = parseActionFromLine(lines[i]);
          if (action) {
            action.line_num = i;
            addAction(action);
          } else {
          }
        }
      }
      if (document.getElementById("sort").checked)
        sortActionsByPrecedence();
      else
        sortActionsById();

      viewUpdateActions();
    }

    var TAG_REGEX = /(\s)([@~$]\w+)([:=]\w+|[:=]{.*})?/g;

    function parseActionFromLine(line) {
      if (line == null || /^\s*$/.test(line))
        return null;
      action = new Object;
      action.line = line;
      match = /^\s*(\S+|\(\S+\)|\[\S+\])\s*:?\s*(.*)$/.exec(line);
      if (match == null)    return null;
      action.priority = match[1];
      action.title    = match[2];
      // Check line format
      if (!match || !action.priority || !action.title)
        return null;

      // Calculate precedence, also check validity of priority
      action.precedence = priorityToPrecedence(action.priority);
      if (action.precedence == -1)
        return null;

      // Mark action status according to its priority
      if (/^[xX]$/.test(action.priority))
        action.status = "completed";
      else
        action.status = "active";

      // Parse tags
      var tags = action.title.match(TAG_REGEX);
      action.tags = new Array;
      if (tags) {
        for (var i = 0; i < tags.length; ++i) {
          action.tags.push(parseTag(trimString(tags[i])));
        }
      }

      return action;
    }

    // Parse a tag string, assuming it starts with a tag symbol.
    function parseTag(tag_str) {
      if (tag_str == null)    return null;
      tag = new Object;
      tag.name   = tag_str.split(/[:=]/)[0];
      value = tag_str.match(/[:=](\w+)|[:=]{(.*)}/);
      tag.value = value ? (value[1] ? value[1] : value[2]) : null;
      return tag;
    }

    var precedence_table = {
      "H": 10, "M": 30, "L": 50,
      "HIGH": 10, "MEDIUM": 30, "LOW": 50,
      "MUST": 10, "SHOULD": 30, "COULD": 50,
      "+": 10, "*": 30, "-": 50,
      "!": 0,                // highest priority
      "~": 100,              // low
      "?": 9000,             // very low
      "x": 9999, "X": 9999   // lowest
    }

    function priorityToPrecedence(priority) {
      if (priority == null)    return -1;
      if (priority[priority.length - 1] == ':')
        priority = priority.substring(0, priority.length - 1);
      var m = /^\((\S+)\)$|^\[(\S+)\]|^(\S+)$/.exec(priority);
      if (m == null)    return -1;
      // Select the first non-null value from three possible matched values
      var pri = m[1] ? m[1] : m[2] ? m[2] : m[3]; 

      // Check the table first
      var p = precedence_table[pri.toUpperCase()];
      if (p != null)    return p;

      // For numbers:
      if (/^\d+$/.test(pri))         return (parseInt(pri) + 1) * 10;

      // For "P0", "P1", ...
      if (/^p\d$/i.test(pri))        return (parseInt(pri.charAt(1)) + 1) * 10;

      // For "A1", "B2", ...
      if (/^[a-e]\d?$/i.test(pri))
        return (pri.toUpperCase().charCodeAt(0) - "A".charCodeAt(0) + 1) * 10
               + (pri.length == 2 ? parseInt(pri[1]) : 0);

      return -1;
    }

    var actions = new Array;  /* global */

    function numActions() { return actions.length }
    function allActions() { return actions }
    function clearActions() { actions = new Array }
    
    function isUrgentAction(a) { return /^(P?0|A|\!)$|must/i.test(a.priority) }

    function sortActionsByPrecedence() {
      actions.sort(function(a,b) { return a.precedence - b.precedence });
    }

    function sortActionsById() {
      actions.sort(function(a,b) { return a.id - b.id });
    }

    function getAction(i) { return actions[i] }
    function setAction(i, a) {
      actions[i] = a;
      actions[i].id = i;
    }

    function addAction(action) {
      action.id = actions.length;
      actions.push(action);
    }

    function delAction(i) {
      actions[i].status = "deleted";
    }

    function setActionStatus(i, s) {
      var old_status = actions[i].status;
      actions[i].status = s;

      if (s == "completed")
        actions[i].priority = "x";
      else if (old_status == "completed")
        actions[i].priority = "?";
      actions[i].precedence = priorityToPrecedence(actions[i].priority);

      return old_status;
    }

    function getActionStatus(i) { return actions[i].status }

    var status_transition = { "active":"completed", "completed":"active" };

    function toggleActionStatus(i) {
      setActionStatus(i, status_transition[actions[i].status]);
    }

    //////// Misc. Utility Functions ////////

    String.prototype.escapeHTML = function() {
      return this.replace(/"/g, "&quot;")
                 .replace(/>/g, "&gt;")
                 .replace(/</g, "&lt;")
                 .replace(/&/g, "&amp;")
    }

    if (!Array.prototype.map)
    {
      Array.prototype.map = function(fun /*, thisp*/)
      {
        var len = this.length;
        if (typeof fun != "function")
          throw new TypeError();

        var res = new Array(len);
        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this)
            res[i] = fun.call(thisp, this[i], i, this);
        }

        return res;
      };
    }

    if (!Array.prototype.filter)
    {
      Array.prototype.filter = function(fun /*, thisp*/)
      {
        var len = this.length;
        if (typeof fun != "function")
          throw new TypeError();

        var res = new Array();
        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this)
          {
            var val = this[i]; // in case fun mutates this
            if (fun.call(thisp, val, i, this))
              res.push(val);
          }
        }

        return res;
      };
    }

    function trimString(s) {
      return s.replace(/^\s*|\s*$/g, '');
    }

    function arrayContains(arr, s) {
      for (var i in arr) {
        if (arr[i] == s)    return true;
      }
      return false;
    }

    function isNull(s) { return s == null }
    function notNull(s) { return s != null }
    function isEmpty(s) { return s.length == 0 }
    function notEmpty(s) { return s.length > 0 }

    // Logic test framework built on Lambda expressions.
    // These high-order operators are quite handy, though a bit hard to catch
    // at first glance. Experience with functional languages could help a lot.
    function True() { return function(x) { return true } }
    function False() { return function(x) { return false } }
    function Eq(y) { return function(x) { return y == x } }
    function Ne(y) { return function(x) { return y != x } }
    function Not(test) { return function(x) { return !test(x) } }
    function And(tests) {
      return function(x) {
        for (i in tests) { if (!tests[i](x)) return false }
        return true;
      }
    }
    function Or(tests) {
      return function(x) {
        for (var i = 0; i < tests.length; ++i) { if (tests[i](x)) return true }
        return false;
      }
    }
    function Any(test) {
      // Apply to an array and return true if any of them passes.
      return function(arr) {
        for (i in arr) { if (test(arr[i])) return true }
        return false;
      }
    }
    function All(test) {
      // Apply to an array and return true iff. all elements pass.
      return function(arr) {
        for (i in arr) { if (!test(arr[i])) return false }
        return true;
      }
    }

    //////// Event Handlers ////////
    function onTextEdit() {
      var e = document.getElementById("new_action");
      e.value = "";
      e.style.color = "black";
    }

    function onTextBlur() {
      // TODO: reset?
      var e = document.getElementById("new_action");
      e.value = "type new action here...";
      e.style.color = "gray";
    }

    function onAdd() {
      action =
          parseActionFromLine(document.getElementById('new_action').value);
      if (action != null)    addAction(action);
      viewUpdateActions();
      document.getElementById("new_action").value = "";
    }

    function onDelete(i) {
      // TODO: fix view update
      delAction(i);
      viewUpdateActions();
    }

    function onEdit(i) {
      var content =
        "<input type=text id=action_edit_" + i +
        " onBlur='javascript:onActionEditBlur(" + i + ")'" +
        " value=\"" + getAction(i).line.escapeHTML() + "\" " +
        "style='border:none'/>";
      document.getElementById("action_desc_" + i).innerHTML = content;
      document.getElementById("action_edit_" + i).select();
    }

    function onActionEditBlur(i) {
      var newline = document.getElementById("action_edit_" + i).value;
      setAction(i, parseActionFromLine(newline));
      viewUpdateActions();
    }

    function onFilter(extra) {
      if (extra)
        document.getElementById("filter").value += " " + extra;
      viewUpdateActions();
    }

    function onClear() {
      document.getElementById("filter").value = "";
      viewUpdateActions();
    }

    function onSort() {
      viewUpdateActions();
    }

    function onToggle(index) {
      toggleActionStatus(index);

      viewUpdateActions();
    }

    function toggleCheckAll() {
      var checked = document.getElementById("check_all").checked;
      var s = checked ? "completed" : "active";
      var items = document.getElementsByName("action");
      for (var i = 0; i < items.length; i++) {
        var index = parseInt(items[i].id.replace("check_", ""));
        setActionStatus(index, s);
      }
      viewUpdateActions();
    }

    function onLoad() {
      sendHttpRequest("./todo.txt", parseDataFileContent);
    }

    //////// HTML View Manipulations ////////

    var current_view = "view";
    function viewSwitchTo(style) {
      current_view = style;
      viewUpdateActions();
    }

    function parseFilterCase(str) {
      if (str[0] == '~' || str[0] == '@' || str[0] == '$') {
        // test for tags
        return function(a) {
          for (var i = 0; i < a.tags.length; ++i)
            if (a.tags[i].name == str)    return true;
          return false;
        }
      } else if (str[0] == '#') {
        // test for priorities
        return function(a) {
          return a.priority == str.substring(1, str.length);
        }
      } else {
        // test for keywords
        return function(a) {
          return a.title.indexOf(str) != -1;
        }
      }
    }

    function parseFilterItem(str) {
      return Or(str.split(/\|+/g).map(parseFilterCase));
    }

    function parseFilterLine(str) {
      return And(str.split(/[,\s]+/g).map(parseFilterItem));
    }

    // Decide if an action should be displayed by checking against filters.
    function shouldDisplay(action) {
      if (action == null || action.status == "deleted")    return false;

      if (isEmpty(document.getElementById("filter").value))  return true;

      var test = parseFilterLine(document.getElementById("filter").value);

      return test(action);
    }

    function viewUpdateActions() {
      if (document.getElementById("sort").checked)
        sortActionsByPrecedence();
      else
        sortActionsById();

      switch(current_view) {
        case "view":
          document.getElementById("actions").innerHTML = viewRenderActions();
          break;
        case "edit":
          document.getElementById("actions").innerHTML = editRenderActions();
          break;
        case "text":
          document.getElementById("actions").innerHTML = textRenderActions();
          break;
        default:
          document.getElementById("actions").innerHTML = "Undefined View: " + current_view;
          break;
      }
    }

    function renderAddAction() {
      return "<form action='javascript:onAdd()'>"
             + "<input type=text id='new_action' size=40 style='color:gray'"
             + "value='type new action here...'"
             + "onclick='javascript:onTextEdit()'"
             + "onblur='javascript:onTextBlur()'/>"
             + "<input type=submit value='Add'/>"
             + "<input type=reset value='Clear'/>"
             + "</form>";
    }

    function viewRenderActions() {
      var all_checked = true;
      var content = "";
      var row = 0;
      for (var i = 0; i < numActions(); ++i) {
        var action = getAction(i);
        if (shouldDisplay(action)) {
          content += "<tr class=" + (row % 2 ? "odd" : "even") + ">";
          content += viewRenderAction(i, action);
          content += "</tr>"
          ++row;
          if (action.status != "completed")    all_checked = false;
        }
      }
      if (row == 0)
        return "No Action.";
      else
        return renderAddAction()
               + "<table>"
               + "<th><input type=checkbox id=check_all "
               + (all_checked ? "checked" : "") + " "
               + "onclick='javascript:toggleCheckAll()'/></th>"
               + "<th>Priority</th><th>Description</th>"
               + "<th>Operations</th>"
               + content
               + "</table>";
    }

    function viewRenderAction(i, action) {
      // Highlight tags
      var title = action.title.replace(TAG_REGEX,
          "$1<span class=tagname onclick=\"javascript:onFilter('$2')\">"
          + "$2</span><span class=tagvalue>$3</span>");

      if (action.status != "completed" && isUrgentAction(action))
        action_class = "urgent";
      else
        action_class = action.status;

      var content;
      content = "<td>"
              + "<input type=checkbox name=action id=check_" + i + " "
              + (action.status == "completed" ? " checked " : " ")
              + "onclick=\"javascript:onToggle(" + i + ")\"/>"
              + "</td>"
              + "<td class='priority " + action_class + "' "
              + "onclick=\"javascript:onFilter('#" + action.priority + "')\">"
              + "<span class='priority " + action_class + "'>"
              + action.priority 
              + "</span>"
              + "</td>"
              + "<td id='action_desc_" + i + "' class='description " + action_class + "'>"
              + "<span class='description " + action_class + "'>" + title + "</span>"
              + "</td>"
              + "<td>&nbsp;&nbsp;"
              + "<span class=op onClick=onEdit(" + i + ")>edit</span>"
              + "&nbsp;&nbsp;"
              + "<span class=op onClick=onDelete(" + i + ")>delete</span>"
              + "&nbsp;&nbsp;</td>";
      return content;
    }

    function textRenderAction(action) {
      // Highlight tags
      var title = action.title.replace(TAG_REGEX,
          "$1<span class=tagname onclick=\"javascript:onFilter('', '$2')\">"
          + "$2</span><span class=tagvalue>$3</span>");

      if (action.status != "completed" && isUrgentAction(action))
        action_class = "urgent";
      else
        action_class = action.status;

      return "<span class='priority " + action_class + "' "
             + "onclick=\"javascript:onFilter('" + action.priority + "', '')\">"
             + action.priority 
             + "</span>&nbsp;&nbsp;"
             + "<span class='description " + action_class + "'>"
             + title
             + "</span>";
    }

    function textRenderActions() {
      var content = "";
      for (var i = 0; i < numActions(); ++i) {
        var action = getAction(i);
        if (shouldDisplay(action))
          content += textRenderAction(action) + "<br>";
      }
      return renderAddAction() + content;
    }

    function onUpdateEdit() {
      parseDataFileContent(document.getElementById('edit').value);
    }

    function editRenderAction(a) {
      return a.priority + "    " + a.title;
    }

    function editRenderActions() {
      var content = "";
      for (var i = 0; i < numActions(); ++i) {
        var action = getAction(i);
        if (shouldDisplay(action))
          content += editRenderAction(action) + "\n";
      }
      return "<textarea rows=80 cols=80 id=edit "
             + "onblur='javascript:onUpdateEdit()'>"
             + content
             + "</textarea>";
    }

    //-->
    </script>
  </head>
  <body onload="javascript:onLoad()">
    <form action="javascript:onFilter()">
      Filter By: <input type="text" id="filter"/>
      <input type="submit" value="Filter"/>
      <input type="reset" value="Clear" onclick="javascript:onClear()"/>
    </form>
    <form>
      <input type="checkbox" id="sort" checked onclick="javascript:onSort()"/>
      <label for="sort">Sort</label>
    </form>
    Switch To: 
    <span class=view_button onclick="javascript:viewSwitchTo('view')">View</span>&nbsp;
    <span class=view_button onclick="javascript:viewSwitchTo('text')">Text</span>&nbsp;
    <span class=view_button onclick="javascript:viewSwitchTo('edit')">Edit</span>&nbsp;
    <br><br>
    <div id="actions">Loading...</div>
    <div id="content" style="display:none">Loading...</div>
  </body>
</html>
